package org.xteam.box2text.oaw;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import org.eclipse.xtend.expression.ExecutionContext;
import org.eclipse.xtend.expression.Variable;
import org.eclipse.xtend.typesystem.Property;
import org.eclipse.xtend.typesystem.Type;
import org.xteam.box2text.box.AOperator;
import org.xteam.box2text.box.Align;
import org.xteam.box2text.box.AlignmentOption;
import org.xteam.box2text.box.BoxFactory;
import org.xteam.box2text.box.GOperator;
import org.xteam.box2text.box.Operator;
import org.xteam.box2text.box.OperatorKinds;
import org.xteam.box2text.parser.ast.Box;
import org.xteam.box2text.parser.ast.BoxSpec;
import org.xteam.box2text.parser.ast.Command;
import org.xteam.box2text.parser.ast.DefaultBoxVisitor;
import org.xteam.box2text.parser.ast.Expression;
import org.xteam.box2text.parser.ast.Function;
import org.xteam.box2text.parser.ast.Ident;
import org.xteam.box2text.parser.ast.IdentValue;
import org.xteam.box2text.parser.ast.IntegerExpression;
import org.xteam.box2text.parser.ast.IntegerValue;
import org.xteam.box2text.parser.ast.ListValue;
import org.xteam.box2text.parser.ast.ModelAccess;
import org.xteam.box2text.parser.ast.Name;
import org.xteam.box2text.parser.ast.Option;
import org.xteam.box2text.parser.ast.StringExpression;
import org.xteam.box2text.parser.ast.StringValue;
import org.xteam.box2text.parser.ast.TextBox;
import org.xteam.box2text.parser.ast.Value;
import org.xteam.parser.runtime.AstList;

public class BoxEvaluator extends DefaultBoxVisitor {

	private BoxExecutionContext ctx;
	private Stack<Operator> operatorStack = new Stack<Operator>();
	private Stack<Object> evaluationStack = new Stack<Object>();
	private Stack<ExecutionContext> contexts = new Stack<ExecutionContext>();
	private org.xteam.box2text.box.Box root;
	private Map<String, Primitive> primitives = new HashMap<String, Primitive>();

	public BoxEvaluator(BoxExecutionContext ctx) {
		this.ctx = ctx;
		addPrimitive(new EscapePrimitive());
		addPrimitive(new NotEmptyPrimitive());
		addPrimitive(new NotNullPrimitive());
	}

	private void addPrimitive(Primitive aPrimitive) {
		primitives.put(aPrimitive.getName(), aPrimitive);
	}

	public org.xteam.box2text.box.Box getResult() {
		return root;
	}
	
	public void visitBox(Box aBox) {
		Operator box = makeBox(aBox.getType().getValue());
		buildOptions(box, aBox.getOptions());
		push(box);
		for (BoxSpec e : aBox.getChildren()) {
			e.visit(this);
		}
		pop();
	}

	public void visitTextBox(TextBox aTextBox) {
		makeText(aTextBox.getContents());
	}
	
	public void visitModelAccess(ModelAccess aModelAccess) {
		Object object = evaluateExpression(aModelAccess.getExpr());
		if (object instanceof Collection) {
			String sep = getSeparator(aModelAccess.getOptions());
			boolean isFirst = true;
			for (Object obj : (Collection) object) {
				if (sep != null && ! isFirst) {
					makeText(sep);
				}
				evaluate(obj);
				isFirst = false;
			}
		} else {
			evaluate(object);
		}
	}
	
	public void visitCommand(Command aCommand) {
		Object object = evaluateExpression(aCommand.getExpr());
		if (aCommand.getName().getValue().equals("if")) {
			if (object.equals(Boolean.TRUE)) {
				aCommand.getArguments().get(0).visit(this);
			} else {
				if (aCommand.getArguments().size() >= 2) {
					aCommand.getArguments().get(1).visit(this);
				}
			}
		}
	}
	
	private Object evaluateExpression(Expression expr) {
		evaluationStack.clear();
		expr.visit(this);
		return evaluationStack.pop();
	}
	
	public void visitName(Name aName) {
		Object current = ctx.getVariable(ExecutionContext.IMPLICIT_VARIABLE).getValue();
		Type t = ctx.getType(current);
		for (Ident elem : aName.getElements()) {
			Property prop = t.getProperty(elem.getValue());
			if (prop == null) {
				current = "%bad%";
				break;
			}
			current = prop.get(current);
			t = ctx.getType(current);
		}
		evaluationStack.push(current);
	}
	
	public void visitFunction(Function aFunction) {
		List<Object> arguments = new ArrayList<Object>();
		for (Expression e : aFunction.getArguments()) {
			e.visit(this);
			arguments.add(evaluationStack.pop());
		}
		Primitive prim = primitives.get(aFunction.getName().getValue());
		evaluationStack.push(prim != null ? prim.evaluate(arguments) : null);
	}

	public void visitIntegerExpression(IntegerExpression aIntegerExpression) {
		evaluationStack.push(new Integer(Integer.decode(aIntegerExpression.getValue())));
	}

	public void visitStringExpression(StringExpression aStringExpression) {
		evaluationStack.push(aStringExpression.getValue());
	}
	
	private void evaluate(Object obj) {
		BoxRule r = ctx.findRule("", ctx.getType(obj));
		if (r != null) {
			pushContext(ctx.cloneWithVariable(new Variable(ExecutionContext.IMPLICIT_VARIABLE, obj)));
			r.getBoxspec().visit(this);
			popContext();
		} else {
			if (obj != null)
				makeText(obj.toString());
		}
	}

	private String getSeparator(AstList<Option> options) {
		for (Option opt : options) {
			if (opt.getName().getValue().equals("sep")) {
				return stringValue(opt.getValue());
			}
		}
		return null;
	}

	private void pushContext(ExecutionContext context) {
		contexts .push(ctx);
		ctx = (BoxExecutionContext) context;
	}
	
	private void popContext() {
		ctx = (BoxExecutionContext) contexts.pop();
	}

	private void makeText(String contents) {
		org.xteam.box2text.box.TextBox tb = BoxFactory.eINSTANCE.createTextBox();
		tb.setContents(contents);
		operatorStack .peek().getArguments().add(tb);
	}
	
	private Operator makeBox(String type) {
		if (type.equals("V"))
			return BoxFactory.eINSTANCE.createVOperator();
		if (type.equals("H"))
			return BoxFactory.eINSTANCE.createHOperator();
		if (type.equals("I"))
			return BoxFactory.eINSTANCE.createIOperator();
		if (type.equals("HV"))
			return BoxFactory.eINSTANCE.createHVOperator();
		if (type.equals("HOV"))
			return BoxFactory.eINSTANCE.createHOVOperator();
		if (type.equals("G"))
			return BoxFactory.eINSTANCE.createGOperator();
		if (type.equals("SL"))
			return BoxFactory.eINSTANCE.createSLOperator();
		if (type.equals("WD"))
			return BoxFactory.eINSTANCE.createWDOperator();
		if (type.equals("A"))
			return BoxFactory.eINSTANCE.createAOperator();
		if (type.equals("R"))
			return BoxFactory.eINSTANCE.createROperator();
		throw new RuntimeException("unknown box type " + type);
	}
	
	private void buildOptions(Operator box, AstList<Option> params) {
		for (Option opt : params) {
			if (box instanceof GOperator && opt.getName().getValue().equals("op")) {
				GOperator gop = (GOperator) box;
				gop.setOperator(OperatorKinds.get(stringValue(opt.getValue())));
			} else if (box instanceof AOperator && opt.getName().getValue().equals("al")) {
				AOperator aop = (AOperator) box;
				aop.getAlignmentOptions().addAll(alignValue(opt.getValue()));
			} else {
				org.xteam.box2text.box.Option o = BoxFactory.eINSTANCE.createOption();
				o.setName(opt.getName().getValue());
				o.setValue(intValue(opt.getValue()));
				box.getOptions().add(o);
			}
		}
	}

	private int intValue(Value value) {
		if (value instanceof IntegerValue) {
			return Integer.decode(((IntegerValue) value).getValue());
		}
		throw new RuntimeException("cannot get int value from " + value);
	}
	
	private String stringValue(Value value) {
		if (value instanceof IdentValue) {
			return((IdentValue) value).getValue();
		}
		if (value instanceof StringValue) {
			return((StringValue) value).getValue();
		}
		throw new RuntimeException("cannot get string value from " + value);
	}
	
	private List<AlignmentOption> alignValue(Value value) {
		List<AlignmentOption> res = new ArrayList<AlignmentOption>();
		if (value instanceof ListValue) {
			for (Ident val : ((ListValue) value).getElements()) {
				AlignmentOption al = BoxFactory.eINSTANCE.createAlignmentOption();
				al.setType(Align.get(val.getValue()));
				res.add(al);
			}
		}
		return res;
	}

	private void push(Operator box) {
		if (operatorStack.size() > 0) {
			Operator current = operatorStack .peek();
			current.getArguments().add(box);
		} else {
			root = box;
		}
		operatorStack.push(box);
	}
	
	private Operator pop() {
		return operatorStack.pop();
	}

}
